/***
 *
 *  ipv4/icmp.c
 *
 *  rtnet - real-time networking subsystem
 *  Copyright (C) 1999, 2000 Zentropic Computing, LLC
 *                2002       Ulrich Marx <marx@kammer.uni-hannover.de>
 *                2002       Vinay Sridhara <vinaysridhara@yahoo.com>
 *                2003-2005  Jan Kiszka <jan.kiszka@web.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#include <linux/types.h>
#include <linux/socket.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <net/checksum.h>

#include <rtskb.h>
#include <rtnet_socket.h>
#include <rtnet_checksum.h>
#include <ipv4_chrdev.h>
#include <ipv4/icmp.h>
#include <ipv4/ip_fragment.h>
#include <ipv4/ip_output.h>
#include <ipv4/protocol.h>
#include <ipv4/route.h>

/***
 * Structure for sending the icmp packets
 */
struct icmp_bxm {
	unsigned int csum;
	size_t head_len;
	size_t data_len;
	off_t offset;
	struct {
		struct icmphdr icmph;
		nanosecs_abs_t timestamp;
	} head;
	union {
		struct rtskb *skb;
		void *buf;
	} data;
};

struct rt_icmp_control {
	void (*handler)(struct rtskb *skb);
	short error; /* This ICMP is classed as an error message */
};

static DEFINE_RTDM_LOCK(echo_calls_lock);
LIST_HEAD(echo_calls);

static struct {
	/*
     * Scratch pad, provided so that rt_socket_dereference(&icmp_socket);
     * remains legal.
     */
	struct rtdm_dev_context dummy;

	/*
     *  Socket for icmp replies
     *  It is not part of the socket pool. It may furthermore be used
     *  concurrently by multiple tasks because all fields are static excect
     *  skb_pool, but that one is spinlock protected.
     */
	struct rtsocket socket;
} icmp_socket_container;

#define icmp_fd (&icmp_socket_container.dummy.fd)
#define icmp_socket ((struct rtsocket *)rtdm_fd_to_private(icmp_fd))

void rt_icmp_queue_echo_request(struct rt_proc_call *call)
{
	rtdm_lockctx_t context;

	rtdm_lock_get_irqsave(&echo_calls_lock, context);
	list_add_tail(&call->list_entry, &echo_calls);
	rtdm_lock_put_irqrestore(&echo_calls_lock, context);
}

void rt_icmp_dequeue_echo_request(struct rt_proc_call *call)
{
	rtdm_lockctx_t context;

	rtdm_lock_get_irqsave(&echo_calls_lock, context);
	list_del(&call->list_entry);
	rtdm_lock_put_irqrestore(&echo_calls_lock, context);
}

void rt_icmp_cleanup_echo_requests(void)
{
	rtdm_lockctx_t context;
	struct list_head *entry;
	struct list_head *next;

	rtdm_lock_get_irqsave(&echo_calls_lock, context);
	entry = echo_calls.next;
	INIT_LIST_HEAD(&echo_calls);
	rtdm_lock_put_irqrestore(&echo_calls_lock, context);

	while (entry != &echo_calls) {
		next = entry->next;
		rtpc_complete_call_nrt((struct rt_proc_call *)entry, -EINTR);
		entry = next;
	}

	/* purge any pending ICMP fragments */
	rt_ip_frag_invalidate_socket(icmp_socket);
}

/***
 *  rt_icmp_discard - dummy function
 */
static void rt_icmp_discard(struct rtskb *skb)
{
}

static int rt_icmp_glue_reply_bits(const void *p, unsigned char *to,
				   unsigned int offset, unsigned int fraglen)
{
	struct icmp_bxm *icmp_param = (struct icmp_bxm *)p;
	struct icmphdr *icmph;
	unsigned long csum;

	/* TODO: add support for fragmented ICMP packets */
	if (offset != 0)
		return -EMSGSIZE;

	csum = rtnet_csum_copy((void *)&icmp_param->head, to,
			       icmp_param->head_len,
			       icmp_param->csum);

	csum = rtskb_copy_and_csum_bits(icmp_param->data.skb,
					icmp_param->offset,
					to + icmp_param->head_len,
					fraglen - icmp_param->head_len, csum);

	icmph = (struct icmphdr *)to;

	icmph->checksum = csum_fold(csum);

	return 0;
}

/***
 *  common reply function
 */
static void rt_icmp_send_reply(struct icmp_bxm *icmp_param, struct rtskb *skb)
{
	struct dest_route rt;
	int err;

	icmp_param->head.icmph.checksum = 0;
	icmp_param->csum = 0;

	/* route back to the source address via the incoming device */
	if (rt_ip_route_output(&rt, skb->nh.iph->saddr, skb->rtdev->local_ip) !=
	    0)
		return;

	rt_socket_reference(icmp_socket);
	err = rt_ip_build_xmit(icmp_socket, rt_icmp_glue_reply_bits, icmp_param,
			       sizeof(struct icmphdr) + icmp_param->data_len,
			       &rt, MSG_DONTWAIT);
	if (err)
		rt_socket_dereference(icmp_socket);

	rtdev_dereference(rt.rtdev);

	RTNET_ASSERT(err == 0,
		     rtdm_printk("RTnet: %s() error in xmit\n", __FUNCTION__););
	(void)err;
}

/***
 *  rt_icmp_echo - handles echo replies on our previously sent requests
 */
static void rt_icmp_echo_reply(struct rtskb *skb)
{
	rtdm_lockctx_t context;
	struct rt_proc_call *call;
	struct ipv4_cmd *cmd;

	rtdm_lock_get_irqsave(&echo_calls_lock, context);

	if (!list_empty(&echo_calls)) {
		call = (struct rt_proc_call *)echo_calls.next;
		list_del(&call->list_entry);

		rtdm_lock_put_irqrestore(&echo_calls_lock, context);
	} else {
		rtdm_lock_put_irqrestore(&echo_calls_lock, context);
		return;
	}

	cmd = rtpc_get_priv(call, struct ipv4_cmd);

	cmd->args.ping.ip_addr = skb->nh.iph->saddr;
	cmd->args.ping.rtt = 0;

	if ((skb->h.icmph->un.echo.id == cmd->args.ping.id) &&
	    (ntohs(skb->h.icmph->un.echo.sequence) ==
	     cmd->args.ping.sequence) &&
	    skb->len == cmd->args.ping.msg_size) {
		if (skb->len >= sizeof(nanosecs_abs_t))
			cmd->args.ping.rtt = rtdm_clock_read() -
					     *((nanosecs_abs_t *)skb->data);
		rtpc_complete_call(call, sizeof(struct icmphdr) + skb->len);
	} else
		rtpc_complete_call(call, 0);
}

/***
 *  rt_icmp_echo_request - handles echo requests sent by other stations
 */
static void rt_icmp_echo_request(struct rtskb *skb)
{
	struct icmp_bxm icmp_param;

	icmp_param.head.icmph = *skb->h.icmph;
	icmp_param.head.icmph.type = ICMP_ECHOREPLY;
	icmp_param.data.skb = skb;
	icmp_param.offset = 0;
	icmp_param.data_len = skb->len;
	icmp_param.head_len = sizeof(struct icmphdr);

	rt_icmp_send_reply(&icmp_param, skb);

	return;
}

static int rt_icmp_glue_request_bits(const void *p, unsigned char *to,
				     unsigned int offset, unsigned int fraglen)
{
	struct icmp_bxm *icmp_param = (struct icmp_bxm *)p;
	struct icmphdr *icmph;
	unsigned long csum;

	/* TODO: add support for fragmented ICMP packets */
	RTNET_ASSERT(
		offset == 0,
		rtdm_printk("RTnet: %s() does not support fragmentation.\n",
			    __FUNCTION__);
		return -1;);

	csum = rtnet_csum_copy((void *)&icmp_param->head, to,
			       icmp_param->head_len,
			       icmp_param->csum);

	csum = rtnet_csum_copy(icmp_param->data.buf,
			       to + icmp_param->head_len,
			       fraglen - icmp_param->head_len, csum);

	icmph = (struct icmphdr *)to;

	icmph->checksum = csum_fold(csum);

	return 0;
}

/***
 *  common request function
 */
static int rt_icmp_send_request(u32 daddr, struct icmp_bxm *icmp_param)
{
	struct dest_route rt;
	unsigned int size;
	int err;

	icmp_param->head.icmph.checksum = 0;
	icmp_param->csum = 0;

	if ((err = rt_ip_route_output(&rt, daddr, INADDR_ANY)) < 0)
		return err;

	/* TODO: add support for fragmented ICMP packets */
	size = icmp_param->head_len + icmp_param->data_len;
	if (size + 20 /* ip header */ >
	    rt.rtdev->get_mtu(rt.rtdev, RT_ICMP_PRIO))
		err = -EMSGSIZE;
	else {
		rt_socket_reference(icmp_socket);
		err = rt_ip_build_xmit(icmp_socket, rt_icmp_glue_request_bits,
				       icmp_param, size, &rt, MSG_DONTWAIT);
		if (err)
			rt_socket_dereference(icmp_socket);
	}

	rtdev_dereference(rt.rtdev);

	return err;
}

/***
 *  rt_icmp_send_echo - sends an echo request to the specified address
 */
int rt_icmp_send_echo(u32 daddr, u16 id, u16 sequence, size_t msg_size)
{
	struct icmp_bxm icmp_param;
	unsigned char *pattern_buf;
	off_t pos;
	int ret;

	/*
	 * This is just setup of a ping message, exec time is not critical, so
	 * rtdm_malloc() is ok here.
	 */
	pattern_buf = rtdm_malloc(msg_size);
	if (pattern_buf == NULL)
		return -ENOMEM;

	/* first purge any potentially pending ICMP fragments */
	rt_ip_frag_invalidate_socket(icmp_socket);

	icmp_param.head.icmph.type = ICMP_ECHO;
	icmp_param.head.icmph.code = 0;
	icmp_param.head.icmph.un.echo.id = id;
	icmp_param.head.icmph.un.echo.sequence = htons(sequence);
	icmp_param.offset = 0;

	if (msg_size >= sizeof(nanosecs_abs_t)) {
		icmp_param.head_len =
			sizeof(struct icmphdr) + sizeof(nanosecs_abs_t);
		icmp_param.data_len = msg_size - sizeof(nanosecs_abs_t);

		for (pos = 0; pos < icmp_param.data_len; pos++)
			pattern_buf[pos] = pos & 0xFF;

		icmp_param.head.timestamp = rtdm_clock_read();
	} else {
		icmp_param.head_len = sizeof(struct icmphdr) + msg_size;
		icmp_param.data_len = 0;

		for (pos = 0; pos < msg_size; pos++)
			pattern_buf[pos] = pos & 0xFF;
	}
	icmp_param.data.buf = pattern_buf;

	ret = rt_icmp_send_request(daddr, &icmp_param);
	rtdm_free(pattern_buf);

	return ret;
}

/***
 *  rt_icmp_socket
 */
int rt_icmp_socket(struct rtdm_fd *fd)
{
	/* we don't support user-created ICMP sockets */
	return -ENOPROTOOPT;
}

static struct rt_icmp_control rt_icmp_pointers[NR_ICMP_TYPES + 1] = {
	/* ECHO REPLY (0) */
	{ rt_icmp_echo_reply, 0 },
	{ rt_icmp_discard, 1 },
	{ rt_icmp_discard, 1 },

	/* DEST UNREACH (3) */
	{ rt_icmp_discard, 1 },

	/* SOURCE QUENCH (4) */
	{ rt_icmp_discard, 1 },

	/* REDIRECT (5) */
	{ rt_icmp_discard, 1 },
	{ rt_icmp_discard, 1 },
	{ rt_icmp_discard, 1 },

	/* ECHO (8) */
	{ rt_icmp_echo_request, 0 },
	{ rt_icmp_discard, 1 },
	{ rt_icmp_discard, 1 },

	/* TIME EXCEEDED (11) */
	{ rt_icmp_discard, 1 },

	/* PARAMETER PROBLEM (12) */
	{ rt_icmp_discard, 1 },

	/* TIMESTAMP (13) */
	{ rt_icmp_discard, 0 },

	/* TIMESTAMP REPLY (14) */
	{ rt_icmp_discard, 0 },

	/* INFO (15) */
	{ rt_icmp_discard, 0 },

	/* INFO REPLY (16) */
	{ rt_icmp_discard, 0 },

	/* ADDR MASK (17) */
	{ rt_icmp_discard, 0 },

	/* ADDR MASK REPLY (18) */
	{ rt_icmp_discard, 0 }
};

/***
 *  rt_icmp_dest_pool
 */
struct rtsocket *rt_icmp_dest_socket(struct rtskb *skb)
{
	rt_socket_reference(icmp_socket);
	return icmp_socket;
}

/***
 *  rt_icmp_rcv
 */
void rt_icmp_rcv(struct rtskb *skb)
{
	struct icmphdr *icmpHdr = skb->h.icmph;
	unsigned int length = skb->len;

	/* check header sanity and don't accept fragmented packets */
	if ((length < sizeof(struct icmphdr)) || (skb->next != NULL)) {
		rtdm_printk("RTnet: improper length in icmp packet\n");
		goto cleanup;
	}

	if (ip_compute_csum((unsigned char *)icmpHdr, length)) {
		rtdm_printk("RTnet: invalid checksum in icmp packet %d\n",
			    length);
		goto cleanup;
	}

	if (!rtskb_pull(skb, sizeof(struct icmphdr))) {
		rtdm_printk("RTnet: pull failed %p\n", (skb->sk));
		goto cleanup;
	}

	if (icmpHdr->type > NR_ICMP_TYPES) {
		rtdm_printk("RTnet: invalid icmp type\n");
		goto cleanup;
	}

	/* sane packet, process it */
	rt_icmp_pointers[icmpHdr->type].handler(skb);

cleanup:
	kfree_rtskb(skb);
}

/***
 *  rt_icmp_rcv_err
 */
void rt_icmp_rcv_err(struct rtskb *skb)
{
	rtdm_printk("RTnet: rt_icmp_rcv err\n");
}

/***
 *  ICMP-Initialisation
 */
static struct rtinet_protocol icmp_protocol = { .protocol = IPPROTO_ICMP,
						.dest_socket =
							&rt_icmp_dest_socket,
						.rcv_handler = &rt_icmp_rcv,
						.err_handler = &rt_icmp_rcv_err,
						.init_socket =
							&rt_icmp_socket };

/***
 *  rt_icmp_init
 */
void __init rt_icmp_init(void)
{
	int skbs;

	skbs = rt_bare_socket_init(icmp_fd, IPPROTO_ICMP, RT_ICMP_PRIO,
				   ICMP_REPLY_POOL_SIZE);
	BUG_ON(skbs < 0);
	if (skbs < ICMP_REPLY_POOL_SIZE)
		printk("RTnet: allocated only %d icmp rtskbs\n", skbs);

	icmp_socket->prot.inet.tos = 0;
	icmp_fd->refs = 1;

	rt_inet_add_protocol(&icmp_protocol);
}

/***
 *  rt_icmp_release
 */
void rt_icmp_release(void)
{
	rt_icmp_cleanup_echo_requests();
	rt_inet_del_protocol(&icmp_protocol);
	rt_bare_socket_cleanup(icmp_socket);
}
